Programming and management of experiments in oTree

Module 2: Individual experiments

Authors
Affiliations

Matteo Ploner

Università di Trento

Luca Congiu

Università degli Studi di Roma “Tor Vergata”

Published

July 2025

Types of experiments

Individual and group experiments

  • In terms of programming, we can identify two main types of experiments:
    • Individual experiments: each participant plays the game alone
      • The payoffs are computed based on the choices of the participant
        • Surveys are a special case of individual experiments without payoffs associated to actions
    • Group experiments: each participant plays the game with other participants
      • The payoffs are computed based on the choices of the participant and the choices of the other participants

Individual experiments

  • Each player chooses independently
    • Payoffs are defined by parameters and by her own choice
  • Each choice is statistically independent from the others

Group experiments

  • Each player chooses independently
    • Payoffs are defined by parameters and by her own choice and the choices of the other players
  • Choices are statistically dependent

Individual experiments

Multiple Price List

  • We investigate individuals’ risk attitudes
  • As an example,
    • Do you prefer a lottery that gives you 2 euros with probability 0.5 and 1.6 euros with probability 0.5, or a lottery that gives you 3.85 euros with probability 0.5 and 0.1 euros with probability 0.5?

Risk attitudes

  • A Multiple Price List format
  • For each pair of options the participants they should choose A or B
    • Option A is safer than corresponding Option B
    • The attractiveness of Option B in terms of relative expected payoffs increases when scrolling down to the bottom of the table
  • A risk-neutral decision maker should switch from A to B at \(5^{th}\) choice
    • Choosing A at \(10^{th}\) choice is dominated for all preference types
  • One row is randomly selected and outcomes paid

Screens

Code

_init_.py (models)

  • Import module random
    • Needed for payoff computation
  • Define session constants
    • Outcomes of lotteries in MPL
  • Group and Subsession classes are empty
    • Typical of one-shot individual decision making
  • Grouping is not defined
import random
from otree.api import *

# Author and description
author = 'MP'
doc = """
MPL risk elicitation à la Holt&Laury
"""

# Constants for the experiment (payoffs, app name, etc.)
class C(BaseConstants):
    NAME_IN_URL = 'MPL'  # App name for URL
    PLAYERS_PER_GROUP = None  # No grouping
    NUM_ROUNDS = 1  # Single round
    # Payoff values for lotteries A and B
    A_h = 2.00  # Lottery A high payoff
    A_l = 1.60  # Lottery A low payoff
    B_h = 3.85  # Lottery B high payoff
    B_l = 0.10  # Lottery B low payoff

# Group class: used for grouping players (not used in this app, but required by oTree)
class Group(BaseGroup):
    pass

# Subsession class: represents a round of the experiment (not used here, but required by oTree)
class Subsession(BaseSubsession):
    pass

_init_.py (models) (ii)

  • In Player class we define the “templates” for data collection
    • MPL table
      • One variable for each row
class Player(BasePlayer):
    # 10 main choices for the MPL table (A or B)
    HL_1 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_2 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_3 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_4 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_5 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_6 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_7 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_8 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_9 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    HL_10 = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal)
    # Demo choice for instructions
    HL = models.CharField(choices=['A', 'B'], widget=widgets.RadioSelectHorizontal, blank=True)

    # Questionnaire fields (demographics and feedback)
    sex = models.StringField(widget=widgets.RadioSelectHorizontal(), choices=['Male', 'Female', 'Other'])
    age = models.IntegerField(choices=range(18, 60, 1))
    comment = models.TextField(label="Your comment here:")
    like = models.IntegerField(choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal)

    # Variables for payoff calculation
    row = models.IntegerField()  # Randomly selected row for payment
    drawn = models.IntegerField()  # Random draw for outcome
    choice = models.CharField()  # Player's choice in selected row

__init.py__ (models) (iii)

  • After Player class we compute the payoffs
    • Typical of individual decision making
# Compute payoff for a player based on random row and outcome
def set_payoff_HL(player: Player):
    # Randomly select a row (1-10) for payment
    player.row = random.randint(1, 10)
    # Randomly select a draw (1-10) to determine outcome
    player.drawn = random.randint(1, 10)
    # Get the player's choice (A or B) for the selected row
    choices = [player.HL_1, player.HL_2, player.HL_3, player.HL_4, player.HL_5,
               player.HL_6, player.HL_7, player.HL_8, player.HL_9, player.HL_10]
    player.choice = choices[player.row - 1]
    # Assign payoff based on choice and draw: if drawn <= row, use high payoff; else, low payoff
    if player.drawn <= player.row:
        player.payoff = float(C.A_h) if player.choice == "A" else float(C.B_h)
    else:
        player.payoff = float(C.A_l) if player.choice == "A" else float(C.B_l)
 

_init.py_ (pages)

  • In the instructions we have a simulation of choice protocol
    • HL (see models)
  • In the main choice page we need to import a form for each row of the MPL table
  • We also need the outcomes, retrieved from constants
    • Important to “declare” the variables to display with vars_for_template()
# Instruction page with demo MPL
class Instructions(Page):
    form_model = 'player'
    form_fields = ['HL']  # Demo choice for instructions

# Main MPL choice page (10 rows)
class PageHL(Page):
    form_model = 'player'
    form_fields = [
        'HL_1', 'HL_2', 'HL_3', 'HL_4', 'HL_5',
        'HL_6', 'HL_7', 'HL_8', 'HL_9', 'HL_10',
    ]  # All 10 choices
    @staticmethod
    def vars_for_template(player: Player):
        # Pass payoff values to template for display
        return {'A_h': C.A_h, 'A_l': C.A_l, 'B_h': C.B_h, 'B_l': C.B_l}
"""
[...]
"""

_init.py_ (pages) (ii)

  • Before moving to the next page, we compute payoffs
    • See method set_payoff_HL() from models.py
      • This way we compute payoffs only once and not when browser is refreshed
class PageHL(Page):

"""
[...]
"""
    # before moving to next page, compute payoffs (avoids that with refreshing payoffs are recomputed again)
    @staticmethod
    def before_next_page(player: Player, timeout_happened):
        # built-in method
        set_payoff_HL(player)  # see in models in Player class

_init.py_ (pages) (iii)

  • Display outcomes
    • declare them with vars_for_template()
      • retrieve values from participant.vars and “store” them in a dictionary
# Outcome page: shows the randomly selected row, draw, and payoff
class OutcomeHL(Page):
    @staticmethod
    def vars_for_template(player: Player):
        # Prepare outcome details for the results template:
        # - row: which row was randomly selected for payment
        # - value: the random draw that determines which outcome is paid
        # - choice: player's choice (A or B) in the selected row
        # - p_A_1, p_A_2, p_B_1, p_B_2: used for displaying lottery probabilities
        return {
            'row': player.row,  # Randomly chosen row
            'value': player.drawn,  # Randomly chosen value
            'choice': player.choice,  # Player's choice
            'p_A_1': player.row,
            'p_A_2': 10 - player.row,
            'p_B_1': player.row,
            'p_B_2': 10 - player.row
        }

_init.py_ (pages) (iv)

  • Manage the sequence of pages

# the coreography of pages
page_sequence = [
                    Instructions,
                    PageHL,
                    OutcomeHL
]

Templates

Instructions.html

  • Style elements
    • size of radio buttons
{{ block styles }}
   <style>
      /* Style radio buttons for better visibility */
      input[type=radio] {
        transform: scale(1.1);
        margin: 12px -10px 0px -30px;
      }
   </style>
{{ endblock }}

Instructions.html (ii)

  • Main body and demo of MPL
    • In a bs container
{{ block content }}

<!-- Main instructions heading -->
<h1>Instructions</h1>

<!-- Instructional content container -->
<div class="container border" style="font-size16pt" >

  <h2> Part 1 </h2>

      <!-- Brief description of the first part -->
      <p>In the first part of the experiment you are going to choose between couples of lotteries. </p>

      <p>The following is an example of the decision setting you are facing</p>
      <!-- Example table for one lottery choice -->
      <table class="table">
        <thead>
          <tr>
            <th scope="col">#</th>
            <th scope="col" colspan="2" style="text-align:center">A</th>
            <th scope="col" ></th>
            <th scope="col" colspan="2"  style="text-align:center">B</th>
            <th scope="col"></th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <th scope="row">1</th>
            <td > p/10 of €A<sub>1</sub>  </td>
            <td> (1-p)/10 of €A<sub>2</sub></td>
            <!-- Radio button for choice between A and B -->
            <td style="font-weight: bold">{{ form.HL }}</td>
            <td>p/10 of €B<sub>1</sub></td>
            <td >(1-p)/10 of €B<sub>2</sub> </td>
          </tr>
        </tbody>
        </table>

<!-- Explanation of the lottery structure -->
<p>You must choose between lottery A and lottery B, with lottery A delivering €A<sub>1</sub> with probability p/10 and €A<sub>2</sub> with probability (1-p)/10.</p>
<p>Similarly, lottery B delivers €B<sub>1</sub> with probability p/10 and €B<sub>2</sub> with probability (1-p)/10.</p> <p>You will face 10 choices between A and B, with p changing across choices.</p>
<p>All your earnings are virtual, no cash is going to be paid to you. However, choose as if the monetary stakes were real.</p>

</div>

Instructions.html (iii)

  • The button to leave the page
    • Put it to the right with a container

"""
[...]
"""

<!-- Continue button row -->
<div class="container" style="font-size:18pt">
  <div class="row" style="padding-left:135px;">
    <div class="col-md-10">
      <!-- Empty column for spacing -->
    </div>
    <div class="col-md-2">
      <!-- Continue button to proceed to next page -->
      <button name="btn_submit" value="True" class="btn btn-outline-primary btn-large">
         <span style="font-size:14pt">Continue</span>
     </button>
    </div>
  </div>
</div>


{{ endblock }}

PageHL.html

  • Choices are collected in a table

  <!-- Table displaying the 10 lottery choices -->
  <table class="table">
  <thead>
    <tr>
      <th scope="col">#</th>
      <th scope="col" colspan="2" style="text-align:center">A</th>
      <th scope="col" ></th>
      <th scope="col" colspan="2"  style="text-align:center">B</th>
      <th scope="col"></th>
    </tr>
  </thead>
  <tbody>
  <!-- Each row represents one lottery choice; radio button in the middle for A/B selection -->
  <!-- Row 1 -->
    <tr>
      <th scope="row">1</th>
      <td > 1/10 of €{{A_h}}  </td>
      <td> 9/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_1 }}</td>
      <td>1/10 of €{{B_h}}</td>
      <td >9/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 2 -->
    <tr>
      <th scope="row">2</th>
      <td>2/10 of €{{A_h}} </td>
      <td> 8/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_2 }}</td>
      <td>2/10 of €{{B_h}}</td>
      <td>8/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 3 -->
    <tr>
      <th scope="row">3</th>
      <td>3/10 of €{{A_h}} </td>
      <td> 7/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_3 }}</td>
      <td>3/10 of €{{B_h}}</td>
      <td>7/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 4 -->
    <tr>
      <th scope="row">4</th>
      <td>4/10 of €{{A_h}} </td>
      <td> 6/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_4 }}</td>
      <td>4/10 of €{{B_h}}</td>
      <td>6/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 5 -->
    <tr>
      <th scope="row">5</th>
      <td>5/10 of €{{A_h}} </td>
      <td> 5/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_5 }}</td>
      <td>5/10 of €{{B_h}}</td>
      <td>5/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 6 -->
    <tr>
      <th scope="row">6</th>
      <td>6/10 of €{{A_h}} </td>
      <td> 4/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_6 }}</td>
      <td>6/10 of €{{B_h}}</td>
      <td>4/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 7 -->
    <tr>
      <th scope="row">7</th>
      <td>7/10 of €{{A_h}} </td>
      <td> 3/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_7 }}</td>
      <td>7/10 of €{{B_h}}</td>
      <td>3/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 8 -->
    <tr>
      <th scope="row">8</th>
      <td>8/10 of €{{A_h}} </td>
      <td> 2/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_8 }}</td>
      <td>8/10 of €{{B_h}}</td>
      <td>2/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 9 -->
    <tr>
      <th scope="row">9</th>
      <td>9/10 of €{{A_h}} </td>
      <td> 1/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_9 }}</td>
      <td>9/10 of €{{B_h}}</td>
      <td>1/10 of €{{B_l}} </td>
    </tr>
  <!-- Row 10 -->
    <tr>
      <th scope="row">10</th>
      <td>10/10 of €{{A_h}} </td>
      <td> 0/10 of €{{A_l}}</td>
      <td style="font-weight: bold">{{ form.HL_10 }}</td>
      <td>10/10 of €{{B_h}}</td>
      <td>0/10 of €{{B_l}} </td>
    </tr>

  </tbody>
  </table>

OutcomeHL.html


<!-- Results page heading -->
<h1> Results </h1>

<!-- Container for the outcome summary and payoff details -->
<div class="container border" style="font-size:14pt">
        <!-- Inform participant which row was selected for payment -->
        <p> <b>Row #{{player.row}}</b> was randomly selected for payment.</p>

        <!-- Table summarizing the selected row's lottery structure -->
        <table class="table">
            <thead>
                <tr>
                    <th scope="col">#</th>
                    <th scope="col" colspan="2" style="text-align:center">A</th>
                    <th scope="col" ></th>
                    <th scope="col" colspan="2"  style="text-align:center">B</th>
                    <th scope="col"></th>
                </tr>
            </thead>
        <tr>
            <th scope="row">{{row}}</th>
            <td>{{p_A_1}}/10 of €{{C.A_h}} </td>
            <td> {{p_A_2}}/10 of €{{C.A_l}}</td>
        <td></td>
            <td>{{p_B_1}}/10 of €{{C.B_h}}</td>
            <td>{{p_B_2}}/10 of €{{C.B_l}} </td>
        </tr>

        </table>
<!-- Show which lottery the participant chose -->
        <p>You chose Lottery <b>{{choice}}</b></p>

<!-- Show the random draw and the resulting payoff -->
<p> The randomly drawn value that defines the outcome of the lottery is {{value}}.</p>

<p> Thus, you earn <b>{{player.payoff}}</b></p>
<!-- Show participant code for reference -->
<p> Your participant code is {{player.participant.code}}</p>
</div>

<!-- Continue button row -->
<div class="container">
    <div class="row">
        &nbsp;
    </div>
    <div class="row" style="padding-left:130px;">
        <div class="col-md-10">
            <!-- Empty column for spacing -->
        </div>
        <div class="col-md-2">
            <!-- Button to proceed to the next page -->
            <button class="otree-btn-next btn btn-primary">
              <span style="font-size:18pt">Continue</span>
            </button>
        </div>
    </div>
</div>


{{ endblock }}
  • The randomly chosen row is displayed {row}
  • They learn about their payoff

An efficient and stylish way to build the table

  • We can use the following code in __init.py__ to build the table (see PageHL_2)
   @staticmethod
    def vars_for_template(player: Player):
        # Build a table of lottery options for display in the template.
        # Each row in the table represents one decision (out of 10),
        # and contains the details for lottery A, lottery B, and the field name for the choice input.
        lottery_table = []
        
        for i in range(1, 11):
            # Define the structure for lottery A in this row
            lottery_a = {
                'prob_high': i,  # Probability (out of 10) of high payoff
                'prob_low': 10 - i,  # Probability (out of 10) of low payoff
                'payoff_high': C.A_h,  # High payoff amount for A
                'payoff_low': C.A_l,   # Low payoff amount for A
                'description': f"{i}/10 chance of €{C.A_h}, {10-i}/10 chance of €{C.A_l}"
            }
            
            # Define the structure for lottery B in this row
            lottery_b = {
                'prob_high': i,  # Probability (out of 10) of high payoff
                'prob_low': 10 - i,  # Probability (out of 10) of low payoff
                'payoff_high': C.B_h,  # High payoff amount for B
                'payoff_low': C.B_l,   # Low payoff amount for B
                'description': f"{i}/10 chance of €{C.B_h}, {10-i}/10 chance of €{C.B_l}"
            }
            
            # Append the row to the table, including the field name for the choice input (e.g., HL_1, HL_2, ...)
            lottery_table.append({
                'row_number': i,
                'lottery_a': lottery_a,
                'lottery_b': lottery_b,
                'choice_field': f'HL_{i}'  # Field name for this row's choice
            })
        
        # Return the table and constants for use in the template
        return {
            'lottery_table': lottery_table,  # List of all rows for the table
            'A_h': C.A_h, 
            'A_l': C.A_l, 
            'B_h': C.B_h, 
            'B_l': C.B_l
        }

An efficient and stylish way to build the table (ii)

  • Then in the template we can use the following code to display the table

  <!-- Main MPL table -->
  <table class="table">
    <!-- Table header -->
    <thead>
      <tr>
        <th>Row</th>
        <th>Lottery A</th>
        <th colspan="2">Your Choice</th> <!-- Spans 2 columns for A and B choices -->
        <th>Lottery B</th>
      </tr>
    </thead>
    <!-- Table body with lottery rows -->
    <tbody>
      <!-- Loop through each row in the lottery table (generated from Python) -->
      {% for row in lottery_table %}
      <tr>
        <!-- Row number (1-10) -->
        <td>{{ row.row_number }}</td>
        <!-- Lottery A description (probabilities and payoffs) -->
        <td>{{ row.lottery_a.description }}</td>
        <!-- Choice A radio button - centered -->
        <td style="text-align: center;">
          <!-- Radio button for choosing lottery A -->
          <input type="radio" name="{{ row.choice_field }}" value="A"> A
        </td>
        <!-- Choice B radio button - centered -->
        <td style="text-align: center;">
          <!-- Radio button for choosing lottery B -->
          <input type="radio" name="{{ row.choice_field }}" value="B"> B
        </td>
        <!-- Lottery B description (probabilities and payoffs) -->
        <td>{{ row.lottery_b.description }}</td>
      </tr>
      {% endfor %}
    </tbody>
  </table>

An efficient and stylish way to build the table (iii)

  • JS to handle the choice selection

{{ block scripts }}
<!-- JavaScript to handle radio button interactions and highlighting -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    // Wait for the DOM to be fully loaded before executing
    
    // Get all radio buttons in the form
    const radioButtons = document.querySelectorAll('input[type="radio"]');
    
    console.log('Found radio buttons:', radioButtons.length); // Debug: log number of radio buttons found
    
    // Add event listener to each radio button for the 'change' event
    radioButtons.forEach(function(radio) {
        radio.addEventListener('change', function() {
            console.log('Radio changed:', this.name, this.value); // Debug: log which radio was changed
            
            // Get the table row that contains this radio button
            const row = this.closest('tr');
            const cells = row.querySelectorAll('td');
            
            console.log('Found cells in row:', cells.length); // Debug: log number of cells in row
            
            // Remove any existing choice classes from all cells in this row
            // This ensures only one lottery is highlighted per row
            cells.forEach(cell => {
                cell.classList.remove('cell-choice-a', 'cell-choice-b');
            });
            
            // Add the appropriate highlighting class based on the selected value
            if (this.value === 'A') {
                // Highlight lottery A column (second cell - index 1)
                if (cells[1]) {
                    cells[1].classList.add('cell-choice-a');
                    console.log('Added cell-choice-a to cell 1'); // Debug: confirm A highlighting
                }
            } else if (this.value === 'B') {
                // Highlight lottery B column (last cell - index 4)
                if (cells[4]) {
                    cells[4].classList.add('cell-choice-b');
                    console.log('Added cell-choice-b to cell 4'); // Debug: confirm B highlighting
                }
            }
        });
        
        // Also add click event as backup in case 'change' doesn't fire
        radio.addEventListener('click', function() {
            // Manually trigger the change event
            this.dispatchEvent(new Event('change'));
        });
    });
    
    // Check for any pre-selected values on page load
    // This handles cases where the form might have existing values
    radioButtons.forEach(function(radio) {
        if (radio.checked) {
            console.log('Pre-selected radio found:', radio.name, radio.value); // Debug: log pre-selected values
            
            const row = radio.closest('tr');
            const cells = row.querySelectorAll('td');
            
            // Remove any existing choice classes from all cells
            cells.forEach(cell => {
                cell.classList.remove('cell-choice-a', 'cell-choice-b');
            });
            
            // Apply highlighting based on pre-selected value
            if (radio.value === 'A') {
                // Highlight lottery A column (second cell - index 1)
                if (cells[1]) {
                    cells[1].classList.add('cell-choice-a');
                }
            } else if (radio.value === 'B') {
                // Highlight lottery B column (last cell - index 4)
                if (cells[4]) {
                    cells[4].classList.add('cell-choice-b');
                }
            }
        }
    });
});
</script>

{{ endblock }} 
  • css to highlight the selected choice

{{ block styles }}
<style>
  /* Radio button styling - make them larger and more visible */
  input[type=radio] {
    transform: scale(1.5);
    /* Make radio buttons 1.5x larger */
    /* margin: 12px -10px 0px -30px; */
    /* Optional margin adjustment (commented out) */
  }

  /* Style for selected radio buttons - blue accent color */
  input[type=radio]:checked {
    accent-color: #007bff;
    /* Bootstrap primary blue */
    background-color: #007bff;
  }

  /* Fallback styling for older browsers that don't support accent-color */
  input[type=radio]:checked::before {
    background-color: #007bff;
  }

  /* Cell background colors for visual feedback when choices are made */
  td.cell-choice-a {
    background-color: #dddddd !important;
    /* Light grey for lottery A selection */
    transition: background-color 0.3s ease;
    /* Smooth color transition */
  }

  td.cell-choice-b {
    background-color: #dddddd !important;
    /* Same grey for lottery B selection */
    transition: background-color 0.3s ease;
    /* Smooth color transition */
  }
</style>
{{ endblock }}

Appendix

Assignment 1

  • Reasonably easy
    • Add a third option in each row: “Indifferent”
      • The choice should look like
  • Difficult
    • Allow people to “switch” only once from A to B
      • As an example, if a participant chooses B in row 4, all rows <4 “automatically” become A and all rows >4 become B

oTree code

  • The oTree app of this lecture:

MPL

References

References

Holt, Charles A, and Susan K Laury. 2002. “Risk Aversion and Incentive Effects.” American Economic Review 92 (5): 1644–55.